LOGIN APP

These steps take you through the process of making a login system.  There are MANY errors in this file.  They are there because I want to emphasize how to debug an app (which is a normal process of writing code!) and the errors are aimed at showing how the names matter and where they come from.  For example, when should I use "Person" instead of "User".  There is a finished webapp without the errors that you can download if you just want a working login system and you don't really care about learning it.

------------------------------------------------------------
In Locomotive make a new app named "login" and run it

------------------------------------------------------------
In MySQL create a db named "login_development"

------------------------------------------------------------
Terminal:

./script/generate model user
./script/generate controller login
./script/generate migration add_first_user

------------------------------------------------------------
Edit: db/migrate/001_create_users.rb

class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.column :first_name, :string
      t.column :last_name, :string
      t.column :short_name, :string
      t.column :hashed_password, :string
    end
  end

  def self.down
    drop_table :users
  end
end

------------------------------------------------------------
Edit: db/migrate/002_add_first_user.rb

class AddFirstUser < ActiveRecord::Migration
  def self.up
    down
    user = Person.create(:first_name => "James",
                           :last_name => "Reynolds",
                           :short_name => "james",
                           :hashed_password => "4d18d61651efa04557ab022844f20ba38d8aba7d", 
                           # letmein!
                           :salt => "284347000.220652855975987")
  end

  def self.down
    Person.delete_all
  end
end

------------------------------------------------------------
Terminal:
	rake db:migrate

Fix 2 errors.  You will have to use "rake db:migrate VERSION=0" to down the migration, and then "rake db:migrate" to up the migration.  Repeat the down and up process until you have fixed all errors (you will know they are all fixed when the last migration proceeds without any errors).

Terminal
	./script/generate scaffold user user_admin

This command will not work if the migrations from above did not complete!

------------------------------------------------------------
Make sure User administration loads:

http://localhost:3001/user_admin

Now add login support!

-----------------------------------------------------------
Edit: app/controllers/application.rb

# Filters added to this controller apply to all controllers in the application.
# Likewise, all the methods added will be available for all controllers.

class ApplicationController < ActionController::Base
  # Pick a unique cookie name to distinguish our session data from others'
  session :session_key => '_login_session_id'

  before_filter :authorize

  private

  def authorize
    unless Person.find_by_id(session[:users_id])
      session[:original_uri] = request.request_uri
      flash[:notice] = "Please log in"
      redirect_to(:controller => "login", :action => "login")
      return false
    end
  end

end

What does this do?  The before_filter runs before any other code.  It will execute the authorize method, which is private, so it can't be executed by sending the browser to http://localhost:3001/application/authorize.  The authorize method searches for the the Person model for the id that is specified in the session hash "users_id".  So somewhere else (login_controller.rb) we need to create that session hash (if the user logs in successfully).  If that session hash is not found, then it saves the uri they are trying to access and then sends the browser to the login page.

------------------------------------------------------------
Edit: app/controllers/login_controller.rb

class LoginController < ApplicationController

  before_filter :authorize, :except => :login

  def login
    session[:user_id] = nil
    if request.post?
      user = Users.authenticate(params[:short_name], params[:password])
      if user
        session[: user_id] = user.id
        uri = session[:original_uri]
        session[:original_uri] = nil
        redirect_to(uri || {:controller => "tasks", :action => "index"} )
      else
        flash[:notice] = "Invalid user/password combination"
      end
    end
  end

  def logout
    session[:user_id] = nil
    flash[:notice] = "You are logged out"
    redirect_to( :action => "login" )
  end

end

What does this do?  This also adds the before filter, except it doesn't run the before filter for the login method.  If it did, then it would never be able to display the login page!  The login method resets the session user_id hash key so it automatically logs a person out.  Then it calls the authenticate method that is present in the Users model, passing in the contents of the short_name and password fields that come from the html form.  If the authenticate method returns an object it means that authentication succeeded.  So it checks for that object (if user) and then sets the session hash key :user_id and either goes to the original url or sends the user to http://localhost:3001/tasks/index.

------------------------------------------------------------
Create file: app/views/login/login.rhtml

<div class="depot-form">
  <fieldset>
    <legend>Please Log In</legend>

    <% form_tag do %>
      <p>
        <label for="login">Login:</label>
        <%= text_field_tag :shortname, params[:short_name] %>
      </p>

      <p>
        <label for="password">Password:</label>
        <%= password_field_tag :password, params[:password] %>
      </p>
      <p>
        <%= submit_tag "Login" %>
      </p>
    <% end %>
  </fieldset>
</div>

This is the html login form.  It creates a form with 2 text fields: shortname and password.  When the submit button is clicked, it will add the values of those 2 fields to the params hash (params[:shortname] and params[:password]).

------------------------------------------------------------
Edit: app/models/user.rb

class User < ActiveRecord::Base

  validates_presence_of :first_name
  validates_presence_of :last_name
  validates_presence_of :short_name
  attr_accessor :password_confirmation
  validates_confirmation_of :password

  def validate
    errors.add_to_base("Missing password") if hashed_password.blank?
  end

  def self.authenticate(short_name, password)
    user = self.find_by_short_name(short_name)
    logger.warn("hi #{user.short_name}!")
    if user
      password_attempt = encrypted_password(password, user.salt)
      logger.warn("Attempted Password #{password_attempt}, real password #{user.hashed_password}")
      if user.hashed_password != password_attempt
        user = nil
      end
    end
    user
  end

  # attribute accessors (to create a temporary field named "password" that is not saved to the database)
  def password
    @password
  end
  def password=(pwd)
    @password = pwd
    create_new_salt
    self.hashed_password = Person.encrypted_password(self.password, self.salt)
  end

  private

  def self.encrypted_password(password, salt)
    string_to_hash = password + "wibble" + salt
    Digest::SHA1.hexdigest(string_to_hash)
  end
  def create_new_salt
    self.salt = self.object_id.to_s + rand.to_s
  end
end

Wow, what does this do.  A lot.  It makes sure that a first_name, last_name, and short_name always exist.  Then it adds temporary field for password_confirmation (attr_accessor).  Then it adds a validation for another temporary field "password" (this temp field is created below).  The temp fields are not saved to the database!  Then it has a custom validator that checks to see if the hashed_password field is blank, and gives an error if it is.

The authenticate method is called in app/controllers/login_controller.rb.  The authenticate method searches the mysql users table for the short_name.  It prints the short_name of that user out.  Then it checks to see if it found a user and encrypts the password they typed in and compares it with the encrypted password stored in the database.  It logs this password, so once this goes into production, the logger line should be removed!

The last methods create the encrypted password (using the Digest library) and a salt (part of the password, making it harder to crack).

------------------------------------------------------------
Now try to load user administration:

http://localhost:3000/user_admin

Fix 6 errors.  Look at the error message.  Sometimes it will tell you the class name that the error occurred.  Remember the class name is very similar to the filenames...  Sometimes the errors will have the file and the line number and actually point to the error!  Those should be easy to fix!

------------------------------------------------------------
Once the login fields show, try to login.  The password for the user is in the migration file!

Notice there is no feedback.  That is because we aren't showing the flash hash yet!

------------------------------------------------------------
Add file: app/views/layouts/login.rhtml

Copy the contents of app/views/layouts/user_admin.rhtml...

------------------------------------------------------------
Try to login as a user that doesn't exist...

Fix errors...

------------------------------------------------------------
You should notice that if you type the correct username/pw it says "Please Login".  But if you type in the wrong password, it says "Incorrect Username/Password".  That indicates that the authentication process actually works, but the part that saves the login status doesn't.  Some errors are "logic" errors, meaning there are no error messages, but things just don't work.... ARG, those suck!  Look for typos and make sure names are consistent in all files.

------------------------------------------------------------

Once you can login, set it up so you can create a new account:

Add to app/views/user_admin/_form.rhtml

<p><label for="user_password">Password</label><br/>
<%= password_field :user, :password %></p>

<p><label for="user_password_confirmation">Confirm</label><br/>
<%= password_field :user, :password_confirmation %></p>

Remove salt and hashed_password fields

------------------------------------------------------------
One more test, go to:

http://localhost:3008/login/logout

Now login again.

Oops, fix that.

------------------------------------------------------------
What happens if you delete all users?  You are told to login, but there are no users in the database!  You can't manually specify the hashed password, so what do you do?  You can either run "rake db:migrate VERSION=1" then "rake db:migrate VERSION=2" to re-run the add_first_user migration.  Or you can comment out "before_filter :authorize" in app/controllers/application.rb and then navigate to localhost:3001/user_admin/new and create a new user.

